#!/usr/bin/env python3

# Copyright (c) 2005-2014 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import abc
import json
import mrget
import shutil
import inspect
import importlib
import subprocess
from ._util import Util
from ._util import TempChdir
from ._util import RangeMatcher
from ._handy import Handy
from ._boot_entry import BootEntryUtils
from ._exception import KernelInitError
from ._exception import KernelSyncError
from ._exception import KernelListVersionsError
from ._exception import KernelFetchError
from ._exception import KernelUnpackError
from ._exception import KernelInstallError
from ._exception import KernelCleanupError
from ._exception import KernelAddonInitError
from ._exception import KernelAddonSyncError
from ._exception import KernelAddonListVersionsError
from ._exception import KernelAddonFetchError
from ._exception import KernelAddonUnpackError
from ._exception import KernelAddonPatchKernelError
from ._exception import KernelAddonModifyKernelConfigError
from ._exception import KernelAddonInstallError
from ._exception import KernelAddonAddKernelInitCmdlineParameterError
from ._exception import InitramfsInitError
from ._exception import InitramfsUnpackError
from ._exception import InitramfsInstallError


class KernelPlugin(abc.ABC):

    InitError = KernelInitError

    SyncError = KernelSyncError

    ListVersionsError = KernelListVersionsError

    FetchError = KernelFetchError

    UnpackError = KernelUnpackError

    InstallError = KernelInstallError

    CleanupError = KernelCleanupError

    def __init__(self, params):
        selfDir = os.path.dirname(os.path.realpath(inspect.getmodule(self.__class__).__file__))

        self.__kernelType = os.path.basename(os.path.dirname(selfDir))

        fullfn = os.path.join(selfDir, "__info__.json")
        if os.path.exists(fullfn):
            with open(fullfn) as f:
                self.__info = json.load(f)
        else:
            self.__info = dict()

        self.__params = params

    @property
    def kernel_type(self):
        return self.__kernelType

    @property
    def description(self):
        return self.__info.get("description", None)

    @property
    def homepage(self):
        return self.__info.get("homepage", None)

    @property
    def license(self):
        return self.__info.get("license", None)

    @property
    def params(self):
        return self.__params

    def sync(self, context):
        pass

    def list_versions(self, context):
        return ["0"]

    def list_distfiles(self, context):
        return []

    def fetch(self, context):
        pass

    def unpack(self, context):
        pass

    def install(self, context):
        pass

    def cleanup(self, context):
        pass


class KernelAddonPlugin(abc.ABC):

    InitError = KernelAddonInitError

    SyncError = KernelAddonSyncError

    ListVersionsError = KernelAddonListVersionsError

    FetchError = KernelAddonFetchError

    UnpackError = KernelAddonUnpackError

    PatchKernelError = KernelAddonPatchKernelError

    ModifyKernelConfigError = KernelAddonModifyKernelConfigError

    InstallError = KernelAddonInstallError

    AddKernelInitCmdlineParameterError = KernelAddonAddKernelInitCmdlineParameterError

    def __init__(self, params):
        selfDir = os.path.dirname(os.path.realpath(inspect.getmodule(self.__class__).__file__))

        self.__kernelType = os.path.basename(os.path.dirname(os.path.dirname(selfDir)))

        self.__name = os.path.basename(selfDir)

        fullfn = os.path.join(selfDir, "__info__.json")
        if os.path.exists(fullfn):
            with open(fullfn) as f:
                self.__info = json.load(f)
        else:
            self.__info = dict()

        self.__params = params

    @property
    def kernel_type(self):
        return self.__kernelType

    @property
    def name(self):
        return self.__name

    @property
    def description(self):
        return self.__info.get("description", None)

    @property
    def homepage(self):
        return self.__info.get("homepage", None)

    @property
    def license(self):
        return self.__info.get("license", None)

    @property
    def compatible_kernel_versions(self):
        return self.__info.get("compatible-kernel-versions", ["*"])

    @property
    def params(self):
        return self.__params

    def sync(self, context):
        pass

    def list_versions(self, context):
        return ["0"]

    def list_distfiles(self, context):
        return []

    def fetch(self, context):
        pass

    def unpack(self, context):
        pass

    def patch_kernel(self, context):
        pass

    def modify_kernel_config(self, context):
        pass

    def install(self, context):
        pass

    def add_kernel_init_cmdline_parameter(self, context):
        pass


class InitramfsPlugin(abc.ABC):

    InitError = InitramfsInitError

    UnpackError = InitramfsUnpackError

    InstallError = InitramfsInstallError

    def __init__(self, params):
        self.__selfDir = os.path.dirname(os.path.realpath(inspect.getmodule(self.__class__).__file__))

        self.__kernelType = os.path.basename(os.path.dirname(self.__selfDir))

        fullfn = os.path.join(self.__selfDir, "__info__.json")
        if os.path.exists(fullfn):
            with open(fullfn) as f:
                self.__info = json.load(f)
        else:
            self.__info = dict()

        self.__params = params

    @property
    def kernel_type(self):
        return self.__kernelType

    @property
    def plugin_dir(self):
        return self.__selfDir

    @property
    def plugin_files_dir(self):
        return os.path.join(self.__selfDir, "files")

    @property
    def description(self):
        return self.__info.get("description", None)

    @property
    def homepage(self):
        return self.__info.get("homepage", None)

    @property
    def license(self):
        return self.__info.get("license", None)

    @property
    def params(self):
        return self.__params

    def unpack(self, context):
        pass

    def patch_kernel(self, context):
        pass

    def modify_kernel_config(self, context):
        pass

    def install(self, context):
        pass


class PluginManager:

    @classmethod
    def has_kernel_plugin(cls, boot_entry_spec):
        return cls._hasPluginDir([boot_entry_spec.kernel_type, "kernel"])

    @classmethod
    def get_kernel_plugin(cls, boot_entry_spec):
        return cls._loadPlugin([boot_entry_spec.kernel_type.value, "kernel"], boot_entry_spec.get_kernel_params())

    @classmethod
    def has_kernel_addon_plugin(cls, boot_entry_spec, kernel_addon_name):
        return cls._hasPluginDir([boot_entry_spec.kernel_type.value, "kernel_addons", kernel_addon_name])

    @classmethod
    def get_kernel_addon_plugin(cls, boot_entry_spec, name):
        return cls._loadPlugin([boot_entry_spec.kernel_type.value, "kernel_addons", name], boot_entry_spec.get_kernel_addon_params(name))

    @classmethod
    def get_kernel_addon_plugins(cls, boot_entry_spec):
        ret = {}
        for name in boot_entry_spec.kernel_addons:
            ret[name] = cls.get_kernel_addon_plugin(boot_entry_spec, name)
        return ret

    @classmethod
    def has_initramfs_plugin(cls, boot_entry_spec):
        return cls._hasPluginDir([boot_entry_spec.kernel_type.value, "initramfs"])

    @classmethod
    def get_initramfs_plugin(cls, boot_entry_spec):
        return cls._loadPlugin([boot_entry_spec.kernel_type.value, "initramfs"], boot_entry_spec.get_initramfs_params())

    @staticmethod
    def _hasPluginDir(subList):
        selfDir = os.path.dirname(os.path.realpath(__file__))
        pluginDir = os.path.join(selfDir, *subList)
        if os.path.exists(pluginDir):
            assert os.path.isdir(pluginDir)
            return True
        else:
            return False

    @staticmethod
    def _loadPlugin(subList, params):
        try:
            modname = ".".join(subList)
            clazz = getattr(importlib.import_module(".%s" % (modname), package=__package__), "Plugin")
            return clazz(params)
        except ModuleNotFoundError:
            assert False


class PluginFetcher:

    def __init__(self, bbki, plugin, cmpFunc):
        self._bbki = bbki
        self._p = plugin
        self._cmpFunc = cmpFunc
        self._version = None

    def do_sync(self):
        context = ContextSync(self._bbki, self._p)
        with TempChdir(self._bbki._fsLayout.get_bbki_cache_distfiles_dir()):
            self._p.sync(context)
        self._version = None

    def do_get_version(self):
        if self._version is not None:
            return self._version

        verExpr = self._p.params.get("version", None)
        if verExpr is not None:
            if verExpr.startswith(">="):
                def cmpFunc2(ver):
                    return Handy.compareKernelVer(ver, verExpr[2:]) >= 0
            elif verExpr.startswith("<="):
                def cmpFunc2(ver):
                    return Handy.compareKernelVer(ver, verExpr[2:]) <= 0
            elif verExpr.startswith(">"):
                def cmpFunc2(ver):
                    return Handy.compareKernelVer(ver, verExpr[1:]) > 0
            elif verExpr.startswith("<"):
                def cmpFunc2(ver):
                    return Handy.compareKernelVer(ver, verExpr[1:]) < 0
            else:
                def cmpFunc2(ver):
                    return Handy.compareKernelVer(ver, verExpr) == 0
        else:
            def cmpFunc2(ver):
                return True

        context = ContextListVersions(self._bbki, self._p)
        with TempChdir(self._bbki._fsLayout.get_bbki_cache_distfiles_dir()):
            lastVer = None
            for ver in self._p.list_versions(context):
                assert lastVer is None or Handy.compareKernelVer(lastVer, ver) > 0      # return value of self._p.list_versions() must be ordered descendly
                if self._cmpFunc(ver) and cmpFunc2(ver):
                    self._version = ver
                    break
                lastVer = ver

        if self._version is None:
            raise self._p.ListVersionsError("no valid version found")
        return self._version

    def do_fetch(self):
        context = ContextFetch(self._bbki, self._p, self.do_get_version())
        with TempChdir(self._bbki._fsLayout.get_bbki_cache_distfiles_dir()):
            self._p.fetch(context)


class KernelPluginFetcher(PluginFetcher):

    def __init__(self, bbki, plugin, addonPlugins):
        self._addonPlugins = addonPlugins
        super().__init__(bbki, plugin, self._cmpFunc)

    def _cmpFunc(self, ver):
        for p in self._addonPlugins:
            if not RangeMatcher(p.compatible_kernel_versions, Handy.compareKernelVer).match(ver):
                return False
        return True


class KernelAddonPluginFetcher(PluginFetcher):

    def __init__(self, bbki, plugin):
        super().__init__(bbki, plugin, lambda x: True)


class PluginExecutor:

    def __init__(self, bbki, plugin, pluginFetcher, name):
        self._bbki = bbki

        self._p = plugin
        self._pf = pluginFetcher

        self._tmpRootDir, self._trTmpDir, self._trWorkDir = _tmpdirs(self._bbki, name)

        Util.forceDelete(self._tmpRootDir)
        os.makedirs(self._trTmpDir)
        os.makedirs(self._trWorkDir)

    def dispose(self):
        Util.forceDelete(self._tmpRootDir)

    def get_work_dir(self):
        return self._trWorkDir

    def get_tmp_dir(self):
        return self._trTmpDir


class KernelPluginExecutor(PluginExecutor):

    def __init__(self, bbki, plugin, pluginFetcher):
        super().__init__(bbki, plugin, pluginFetcher, "kernel")

    def do_unpack(self):
        self._pf.do_fetch()
        context = ContextUnpack(self._bbki, self._p, self._pf, self)
        with TempChdir(self._trWorkDir):
            self._p.unpack(context)

    def do_install(self, targetBootEntry):
        context = ContextKernelInstall(self._bbki, self._pf, targetBootEntry)
        with TempChdir(self._trWorkDir):
            assert os.path.exists(".config")
            self._p.install(context)

    def do_cleanup(self, targetBootEntry):
        context = ContextCleanup(self._bbki, self._pf, targetBootEntry)
        with TempChdir(self._trWorkDir):
            self._p.cleanup(context)


class KernelAddonPluginExecutor(PluginExecutor):

    def __init__(self, bbki, kernelPluginFetcher, kernelPluginExecutor, plugin, pluginFetcher):
        super().__init__(bbki, plugin, pluginFetcher, "kernel_addon_" + plugin.name)
        self._kernelPluginFetcher = kernelPluginFetcher
        self._kernelPluginExecutor = kernelPluginExecutor

    def do_unpack(self):
        self._pf.do_fetch()
        context = ContextUnpack(self._bbki, self._p, self._pf, self)
        with TempChdir(self._trWorkDir):
            self._p.unpack(context)

    def do_patch_kernel(self):
        context = ContextKernelAddonPatchKernel(self._bbki, self._pf, self._kernelPluginFetcher, self._kernelPluginExecutor)
        with TempChdir(self._trWorkDir):
            self._p.patch_kernel(context)

    def do_modify_kernel_config(self, makeConfig):
        context = ContextKernelAddonModifyKernelConfig(self._bbki, self._pf, self._kernelPluginFetcher, self._kernelPluginExecutor, makeConfig)
        with TempChdir(self._trWorkDir):
            self._p.modify_kernel_config(context)

    def do_install(self, targetBootEntry):
        context = ContextKernelAddonInstall(self._bbki, self._pf, self._kernelPluginFetcher, self._kernelPluginExecutor, targetBootEntry)
        with TempChdir(self._trWorkDir):
            self._p.install(context)


class InitramfsPluginExecutor(PluginExecutor):

    def __init__(self, bbki, kernelPluginFetcher, kernelPluginExecutor, plugin):
        super().__init__(bbki, plugin, None, "initramfs")
        self._kernelPluginFetcher = kernelPluginFetcher
        self._kernelPluginExecutor = kernelPluginExecutor

    def do_unpack(self):
        context = ContextUnpack(self._bbki, self._p, self._pf, self)
        with TempChdir(self._trWorkDir):
            self._p.unpack(context)

    def do_patch_kernel(self):
        with TempChdir(self._trWorkDir):
            self._p.patch_kernel(None)

    def do_modify_kernel_config(self, makeConfig):
        context = ContextInitramfsModifyKernelConfig(self._bbki, self._kernelPluginFetcher, self._kernelPluginExecutor, makeConfig)
        with TempChdir(self._trWorkDir):
            self._p.modify_kernel_config(context)

    def do_install(self, targetBootEntry):
        context = ContextInitramfsInstall(self._bbki, self._trTmpDir, targetBootEntry)
        with TempChdir(self._trWorkDir):
            self._p.install(context)


class ContextSync:

    def __init__(self, bbki, plugin):
        self._bbki = bbki
        self._p = plugin

    def distfiles_open_for_write(self, fn, mode=None):
        return _distfiles_open_for_write(self._bbki, fn, mode, self._p.SyncError)

    def distfiles_download(self, url, fn=None, checksum_algorithm=None, checksum=None):
        return _distfiles_download(self._bbki, url, fn, checksum_algorithm, checksum, self._p.SyncError)

    def distfiles_bittorrent_download(self, url, fn=None):
        # FIXME
        assert False

    def distfiles_git_clone_or_update(self, url, fn=None, commit_or_tag=None):
        return _distfiles_git_clone_or_update(self._bbki, url, None, fn, commit_or_tag, self._p.SyncError)

    def distfiles_github_clone_or_update(self, url, fn=None, commit_or_tag=None):
        return _distfiles_git_clone_or_update(self._bbki, url,
                                              {"remote.origin.fetch": "+refs/pull/*/head:refs/remotes/origin/pr/*"},
                                              fn, commit_or_tag, self._p.SyncError)

    def distfiles_gitlab_clone_or_update(self, url, fn=None, commit_or_tag=None):
        return _distfiles_git_clone_or_update(self._bbki, url,
                                              {"remote.origin.fetch": "+refs/merge-requests/*/head:refs/remotes/origin/mr/*"},
                                              fn, commit_or_tag, self._p.SyncError)

    def distfiles_subversion_checkout_or_update(self, url, fn=None):
        return _distfiles_subversion_checkout_or_update(self._bbki, url, fn, self._p.SyncError)


class ContextListVersions:

    def __init__(self, bbki, plugin):
        self._bbki = bbki
        self._p = plugin

    def distfiles_find(self, fn):
        return _distfiles_find(self._bbki, fn)

    def distfiles_git_find(self, fn):
        return _distfiles_git_find(self._bbki, fn)

    def distfiles_subversion_find(self, fn):
        return _distfiles_subversion_find(self._bbki, fn)

    def distfiles_get(self, fn):
        return _distfiles_get(self._bbki, fn, self._p.FetchError)

    def distfiles_git_get(self, fn):
        return _distfiles_git_get(self._bbki, fn, self._p.FetchError)

    def distfiles_subversion_get(self, fn):
        return _distfiles_subversion_get(self._bbki, fn, self._p.FetchError)


class ContextListDistfiles:

    def __init__(self, bbki, plugin, version):
        self._bbki = bbki
        self._p = plugin
        self._versionList = [version]

    @property
    def versions(self):
        return self._versionList

    def distfiles_fn(self, fn):
        return fn

    def distfiles_git_fn(self, fn):
        return os.path.join("git-src", fn)

    def distfiles_subversion_fn(self, fn):
        return os.path.join("svn-src", fn)


class ContextFetch:

    def __init__(self, bbki, plugin, version):
        self._bbki = bbki
        self._p = plugin
        self._versionList = [version]

    @property
    def versions(self):
        return self._versionList

    def distfiles_find(self, fn):
        return _distfiles_find(self._bbki, fn)

    def distfiles_git_find(self, fn):
        return _distfiles_git_find(self._bbki, fn)

    def distfiles_subversion_find(self, fn):
        return _distfiles_subversion_find(self._bbki, fn)

    # FIXME: bad
    def distfiles_git_has_commit_or_tag(self, fn, commit_or_tag):
        return _distfiles_git_has_commit_or_tag(self._bbki, fn, commit_or_tag)

    # FIXME: bad
    def distfiles_subversion_has_commit_or_tag(self, fn, commit_or_tag):
        return _distfiles_subversion_has_commit_or_tag(self._bbki, fn, commit_or_tag)

    def distfiles_open_for_write(self, fn, mode=None):
        return _distfiles_open_for_write(self._bbki, fn, mode, self._p.FetchError)

    def distfiles_download(self, url, fn=None, checksum_algorithm=None, checksum=None):
        return _distfiles_download(self._bbki, url, fn, checksum_algorithm, checksum, self._p.FetchError)

    def distfiles_bittorrent_download(self, url, fn=None):
        # FIXME
        assert False

    def distfiles_git_clone_or_update(self, url, fn=None, commit_or_tag=None):
        return _distfiles_git_clone_or_update(self._bbki, url, None, fn, commit_or_tag, self._p.FetchError)

    def distfiles_github_clone_or_update(self, url, fn=None, commit_or_tag=None):
        return _distfiles_git_clone_or_update(self._bbki, url,
                                              {"remote.origin.fetch": "+refs/pull/*/head:refs/remotes/origin/pr/*"},
                                              fn, commit_or_tag, self._p.SyncError)

    def distfiles_gitlab_clone_or_update(self, url, fn=None, commit_or_tag=None):
        return _distfiles_git_clone_or_update(self._bbki, url,
                                              {"remote.origin.fetch": "+refs/merge-requests/*/head:refs/remotes/origin/mr/*"},
                                              fn, commit_or_tag, self._p.SyncError)

    def distfiles_subversion_checkout_or_update(self, url, fn=None):
        return _distfiles_subversion_checkout_or_update(self._bbki, url, fn, self._p.FetchError)

    def distfiles_get(self, fn):
        return _distfiles_get(self._bbki, fn, self._p.FetchError)

    def distfiles_git_get(self, fn):
        return _distfiles_git_get(self._bbki, fn, self._p.FetchError)

    def distfiles_subversion_get(self, fn):
        return _distfiles_subversion_get(self._bbki, fn, self._p.FetchError)


class ContextUnpack:

    def __init__(self, bbki, plugin, fetcher, executor):
        self._bbki = bbki
        self._p = plugin
        self._pf = fetcher
        self._executor = executor

    @property
    def version(self):
        return self._pf.do_get_version()

    def get_tmp_dir(self):
        return self._p.get_tmp_dir()

    def distfiles_get(self, fn):
        return _distfiles_get(self._bbki, fn, self._p.UnpackError)

    def distfiles_git_get(self, fn):
        return _distfiles_git_get(self._bbki, fn, self._p.UnpackError)

    def distfiles_subversion_get(self, fn):
        return _distfiles_subversion_get(self._bbki, fn, self._p.UnpackError)

    # FIXME: bad
    def distfiles_git_use(self, fn, commit_or_tag=None, subdir=None):
        return _distfiles_git_use(self._bbki, fn, commit_or_tag, self._executor.get_work_dir(), subdir, self._p.UnpackError)

    # FIXME: bad
    def distfiles_subversion_use(self, fn, commit_or_tag=None, subdir=None):
        return _distfiles_subversion_use(self._bbki, fn, commit_or_tag, self._executor.get_work_dir(), subdir, self._p.UnpackError)


class ContextKernelInstall:

    def __init__(self, bbki, pluginFetcher, targetBootEntry):
        self._bbki = bbki
        self._pf = pluginFetcher
        self._be = targetBootEntry

    @property
    def version(self):
        return self._pf.do_get_version()

    def distfiles_get(self, fn):
        return _distfiles_get(self._bbki, fn, KernelInstallError)

    def distfiles_git_get(self, fn):
        return _distfiles_git_get(self._bbki, fn, KernelInstallError)

    def distfiles_subversion_get(self, fn):
        return _distfiles_subversion_get(self._bbki, fn, KernelInstallError)

    # FIXME: bad
    def distfiles_git_use(self, fn, commit_or_tag=None, subdir=None):
        return _distfiles_git_use(self._bbki, fn, commit_or_tag, self._executor.get_work_dir(), subdir, KernelInstallError)

    # FIXME: bad
    def distfiles_subversion_use(self, fn, commit_or_tag=None, subdir=None):
        return _distfiles_subversion_use(self._bbki, fn, commit_or_tag, self._executor.get_work_dir(), subdir, KernelInstallError)

    def get_build_variable(self, var_name):
        return self._bbki._cfg.get_build_variable(var_name)

    def get_boot_entry(self):
        return self._be


class ContextKernelAddonPatchKernel:

    def __init__(self, bbki, pluginFetcher, kernelPluginFetcher, kernelPluginExecutor):
        self._bbki = bbki
        self._pf = pluginFetcher
        self._kernelPluginFetcher = kernelPluginFetcher
        self._kernelPluginExecutor = kernelPluginExecutor

    @property
    def version(self):
        return self._pf.do_get_version()

    def get_kernel_version(self):
        return self._kernelPluginFetcher.do_get_version()

    def get_kernel_work_dir(self):
        return self._kernelPluginExecutor.get_work_dir()


class ContextKernelAddonModifyKernelConfig:

    def __init__(self, bbki, pluginFetcher, kernelPluginFetcher, kernelPluginExecutor, makeConfig):
        self._bbki = bbki
        self._pf = pluginFetcher
        self._kernelPluginFetcher = kernelPluginFetcher
        self._kernelPluginExecutor = kernelPluginExecutor
        self._makeConfig = makeConfig

    @property
    def ValuePriority(self):
        return self._makeConfig.ValuePriority

    @property
    def version(self):
        return self._pf.do_get_version()

    def get_kernel_version(self):
        return self._kernelPluginFetcher.do_get_version()

    def __getattr__(self, attr):
        prefixList = [
            "select_by_",
            "select_tree_by_",
            "set_integer_by_",
            "set_string_by_",
            "set_choice_by_",
            "list_symbols_in_tree_by_",
        ]
        if any([attr.startswith(x) for x in prefixList]):
            return getattr(self._makeConfig, attr)

        # ugly, from PEP-562, a string which is same as system error message
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attr}'")


class ContextKernelAddonInstall:

    def __init__(self, bbki, pluginFetcher, kernelPluginFetcher, kernelPluginExecutor, targetBootEntry):
        self._bbki = bbki
        self._pf = pluginFetcher
        self._kernelPluginFetcher = kernelPluginFetcher
        self._kernelPluginExecutor = kernelPluginExecutor
        self._be = targetBootEntry

    @property
    def version(self):
        return self._pf.do_get_version()

    def distfiles_get(self, fn):
        return _distfiles_get(self._bbki, fn, KernelAddonInstallError)

    def get_build_variable(self, var_name):
        return self._bbki._cfg.get_build_variable(var_name)

    def get_kernel_version(self):
        return self._kernelPluginFetcher.do_get_version()

    def get_kernel_work_dir(self):
        return self._kernelPluginExecutor.get_work_dir()

    def get_boot_entry(self):
        return self._be

    def install_firmware_file(self, file_path, target):
        ret = []
        for fp in BootEntryUtils.get_firmware_filenames(self._be):
            if fp == target:
                dstFullfn = os.path.join(self._be.firmware_dirpath, fp)
                subprocess.check_call(["install", "-D", "-m0644", "-o", "0", "-g", "0", file_path, dstFullfn])
                ret.append(fp)
        assert len(ret) in [0, 1]
        return ret

    def install_firmware_files_in_directory(self, dir_path):
        ret = []
        for fp in BootEntryUtils.get_firmware_filenames(self._be):
            if os.path.exists(os.path.join(dir_path, fp)):
                Util.copyWithSymlink(dir_path, self._be.firmware_dirpath, fp, uid=0, gid=0, mode=0o0644)
                ret.append(fp)
        return ret

    def install_extra_firmware_file(self, file_path, target):
        dstFullfn = os.path.join(self._be.firmware_dirpath, target)
        subprocess.check_call(["install", "-D", "-m0644", "-o", "0", "-g", "0", file_path, dstFullfn])
        Util.appendOrCreateListFile(os.path.join(self._be.firmware_dirpath, "firmware.extra-files"), target)
        return [target]


class ContextKernelAddonAddKernelInitCmdlineParameter:

    def __init__(self, bbki):
        self._bbki = bbki
        self._kernelVersion = None      # FIXME
        self._result = []

    def get_kernel_version(self):
        return self._kernelVersion

    def add_kernel_init_cmdline_parameter(self, parameter):
        self._result.append(parameter)


class ContextInitramfsModifyKernelConfig:

    def __init__(self, bbki, kernelPluginFetcher, kernelPluginExecutor, makeConfig):
        self._bbki = bbki
        self._kernelPluginFetcher = kernelPluginFetcher
        self._kernelPluginExecutor = kernelPluginExecutor
        self._makeConfig = makeConfig

    def get_kernel_version(self):
        return self._kernelPluginFetcher.do_get_version()

    def __getattr__(self, attr):
        prefixList = [
            "select_by_",
            "select_tree_by_",
            "set_integer_by_",
            "set_string_by_",
            "set_choice_by_",
            "list_symbols_in_tree_by_",
        ]
        if any([attr.startswith(x) for x in prefixList]):
            return getattr(self._makeConfig, attr)

        # ugly, from PEP-562, a string which is same as system error message
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attr}'")


class ContextInitramfsInstall:

    def __init__(self, bbki, tmpDir, targetBootEntry):
        self._bbki = bbki
        self._tmpDir = tmpDir
        self._be = targetBootEntry

    def get_tmp_dir(self):
        return self._tmpDir

    def distfiles_get(self, fn):
        return _distfiles_get(self._bbki, fn, InitramfsInstallError)

    def get_build_variable(self, var_name):
        return self._bbki._cfg.get_build_variable(var_name)

    def get_boot_entry(self):
        return self._be

    def get_mount_points(self):
        return self._bbki._cfg.get_mount_points()

    def get_system_init(self):
        return self._bbki._cfg.get_system_init()


class ContextCleanup:

    def __init__(self, bbki, pluginFetcher, targetBootEntry):
        self._bbki = bbki
        self._pf = pluginFetcher
        self._be = targetBootEntry

    @property
    def version(self):
        return self._pf.do_get_version()

    def get_boot_entry(self):
        return self._be


def _tmpdirs(bbki, name):
    tmpRootDir = os.path.join(bbki._fsLayout.get_bbki_tmp_dir(), name)
    # trBuildInfoDir = os.path.join(tmpRootDir, "build-info")     # FIXME
    # trDistDir = os.path.join(tmpRootDir, "distdir")             # FIXME
    # trEmptyDir = os.path.join(tmpRootDir, "empty")              # FIXME
    # trFilesDir = os.path.join(tmpRootDir, "files")              # should be symlink
    # trHomeDir = os.path.join(tmpRootDir, "homedir")             # FIXME
    trTmpDir = os.path.join(tmpRootDir, "temp")
    trWorkDir = os.path.join(tmpRootDir, "work")
    return (tmpRootDir, trTmpDir, trWorkDir)


def _distfiles_find(bbki, fn):
    for fulld in bbki._fsLayout.get_bbki_cache_distfiles_ro_dir_list():
        fullfn = os.path.join(fulld, fn)
        if os.path.exists(fullfn):
            return fullfn

    fullfn = os.path.join(bbki._fsLayout.get_bbki_cache_distfiles_dir(), fn)
    if os.path.exists(fullfn):
        return fullfn

    return None


def _distfiles_git_find(bbki, fn):
    return _distfiles_find(bbki, os.path.join("git-src", fn))


def _distfiles_subversion_find(bbki, fn):
    return _distfiles_find(bbki, os.path.join("svn-src", fn))


def _distfiles_git_has_commit_or_tag(bbki, fn, commit_or_tag):
    fullfn = _distfiles_git_find(bbki, fn)
    if fullfn is None:
        return False
    return Util.gitHasCommitOrTag(fullfn, commit_or_tag)


def _distfiles_subversion_has_commit_or_tag(bbki, fn, commit_or_tag):
    fullfn = _distfiles_subversion_find(bbki, fn)
    if fullfn is None:
        return False

    # FIXME
    assert False


def _distfiles_open_for_write(bbki, fn, mode, exceptionClass):
    if mode is None:
        mode = "w"
    elif mode == "":
        mode = "w"
    elif mode == "b":
        mode = "wb"
    else:
        assert False

    for fulld in bbki._fsLayout.get_bbki_cache_distfiles_ro_dir_list():
        if os.path.exists(os.path.join(fulld, fn)):
            raise exceptionClass("file \"%s\" already exists in \"%s\"" % (fn, fulld))

    localFullFn = os.path.join(bbki._fsLayout.get_bbki_cache_distfiles_dir(), fn)
    return open(localFullFn, mode)


def _distfiles_download(bbki, url, fn, checksum_algorithm, checksum, exceptionClass):
    if fn is None:
        fn = os.path.basename(url)

    for fulld in bbki._fsLayout.get_bbki_cache_distfiles_ro_dir_list():
        if os.path.exists(os.path.join(fulld, fn)):
            raise exceptionClass("file \"%s\" already exists in \"%s\"" % (fn, fulld))

    localFullFn = os.path.join(bbki._fsLayout.get_bbki_cache_distfiles_dir(), fn)
    while os.path.exists(localFullFn):
        if not os.path.isfile(localFullFn) or os.path.islink(localFullFn):
            Util.forceDelete(localFullFn)
            break
        if checksum_algorithm is not None and not Util.verifyChecksum(localFullFn, checksum_algorithm, checksum):
            Util.forceDelete(localFullFn)
            break
        return localFullFn

    if url.startswith("mirror://"):
        url = mrget.target_urls(url)[0]

    tmpLocalFullFn = localFullFn + ".__download__"
    subprocess.check_call(["wget", "-O", tmpLocalFullFn, url])
    if checksum_algorithm is not None and not Util.verifyChecksum(tmpLocalFullFn, checksum_algorithm, checksum):
        raise exceptionClass("invalid content for \"%s\"" % (url))
    os.rename(tmpLocalFullFn, localFullFn)

    return localFullFn


def _distfiles_git_clone_or_update(bbki, clone_url, clone_config, fn, commit_or_tag, exceptionClass):
    if fn is None:
        fn = os.path.basename(clone_url)

    for fulld in bbki._fsLayout.get_bbki_cache_distfiles_ro_dir_list():
        if os.path.exists(os.path.join(fulld, "git-src", fn)):
            raise exceptionClass("directory \"%s\" already exists in \"%s\"" % (fn, fulld))

    localFullFn = os.path.join(bbki._fsLayout.get_bbki_cache_distfiles_dir(), "git-src", fn)
    if os.path.exists(localFullFn):
        if os.path.isdir(localFullFn) and not os.path.islink(localFullFn):
            if commit_or_tag is not None and Util.gitHasCommitOrTag(localFullFn, commit_or_tag):
                return localFullFn
        else:
            Util.forceDelete(localFullFn)

    Util.gitPull(localFullFn, reclone_on_failure=True, clone_url=clone_url, clone_config=clone_config)
    if commit_or_tag is not None and not Util.gitHasCommitOrTag(localFullFn, commit_or_tag):
        raise exceptionClass("commit or tag \"%s\" does not exist")

    return localFullFn


def _distfiles_subversion_checkout_or_update(bbki, url, fn, exceptionClass):
    if fn is None:
        fn = os.path.basename(url)

    for fulld in bbki._fsLayout.get_bbki_cache_distfiles_ro_dir_list():
        if os.path.exists(os.path.join(fulld, "svn-src", fn)):
            raise exceptionClass("directory \"%s\" already exists in \"%s\"" % (fn, fulld))

    if url.startswith("mirror://"):
        url = mrget.target_urls(url)[0]

    # FIXME
    assert False


def _distfiles_get(bbki, fn, exceptionClass):
    ret = _distfiles_find(bbki, fn)
    if ret is None:
        raise exceptionClass("file %s not found in distfile directories" % (fn))
    return ret


def _distfiles_git_get(bbki, fn, exceptionClass):
    ret = _distfiles_git_find(bbki, fn)
    if ret is None:
        raise exceptionClass("directory %s not found in distfile directories" % (fn))
    return ret


def _distfiles_subversion_get(bbki, fn, exceptionClass):
    ret = _distfiles_subversion_find(bbki, fn)
    if ret is None:
        raise exceptionClass("directory %s not found in distfile directories" % (fn))
    return ret


# FIXME: should not be here
def _distfiles_git_use(bbki, fn, commit_or_tag, workDir, subDir, exceptionClass):
    ret = _distfiles_git_get(bbki, fn, exceptionClass)
    dstDir = workDir if subDir is None else os.path.join(workDir, subDir)
    shutil.copytree(ret, dstDir, dirs_exist_ok=True)
    if commit_or_tag is not None:
        # FIXME: suck that I can't find how pygit2 can do the same
        with TempChdir(dstDir):
            subprocess.check_output(["git", "checkout", commit_or_tag], stderr=subprocess.STDOUT)
    return dstDir


# FIXME: should not be here
def _distfiles_subversion_use(bbki, fn, commit_or_tag, workDir, subDir, exceptionClass):
    # FIXME
    assert False
